package icasue.net.protocol.http.utils;

import com.alibaba.fastjson.JSON;
import icasue.net.base.annotations.NotNull;
import icasue.net.base.annotations.Nullable;
import icasue.net.protocol.http.HttpHelper;
import icasue.net.protocol.http.HttpResponse;

import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.List;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2021/4/9 下午2:28
 * @UpdateDate:
 * @Description:
 */
public class SocketUtil {

    private static final Charset CHARSET_DEFAULT = Charset.defaultCharset();
    private static final int ZERO = 0;

    private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
            | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
    public static final MethodHandles.Lookup methodLookup;

    static {
        //get lookup
        MethodHandles.Lookup lookupTemp = null;
        try {
            //java 9
            Method lookupMethod = MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
            lookupMethod.setAccessible(true);
            MethodHandles.Lookup lookup =
                    (MethodHandles.Lookup)lookupMethod.invoke(null, MethodHandles.Lookup.class, MethodHandles.lookup());
            lookupTemp = lookup;
        }catch (Exception e){
            //java 8
            try {
                Constructor<MethodHandles.Lookup> lookupConstruct = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
                lookupConstruct.setAccessible(true);
                lookupTemp = lookupConstruct.newInstance(MethodHandles.Lookup.class, ALLOWED_MODES);
            } catch (Exception e1) {
                throw new RuntimeException(e1.getCause());
            }
        }
        methodLookup = lookupTemp;
    }

    /**
     * Send socket request by one fixed host and port, once occur SocketException will be throwing!
     * @param host
     * @param port
     * @param https
     * @param charset
     * @param protocol
     * @param type
     * @param socketTimeOutMillions SocketTimeOut max Millions, valid range: (0,)
     * @param <T>
     * @return
     */
    public static <T> HttpResponse<T> send(@NotNull String host, @NotNull Integer port, boolean https, @NotNull StringBuilder protocol,
                                           @Nullable Charset charset, @NotNull Class<T> type,
                                           @Nullable Integer socketTimeOutMillions) throws IOException {
        charset = charset == null ? CHARSET_DEFAULT : charset;
        Socket socket = null;
        try {
            InetSocketAddress dist = new InetSocketAddress(host, port);
            if(!https){
                socket = new Socket();
            }else {
                socket = SSLSocketFactory.getDefault().createSocket();
            }
            if(socketTimeOutMillions != null && socketTimeOutMillions > ZERO){
                socket.setSoTimeout(socketTimeOutMillions);
            }
            socket.connect(dist);

            OutputStream outputStream = socket.getOutputStream();
            InputStream inputStream = socket.getInputStream();

            outputStream.write(protocol.toString().getBytes(charset));
            outputStream.flush();
            if(!https){
                socket.shutdownOutput();
            }

            HttpResponse RES = HttpHelper.parseInternal(inputStream, charset);
            if(RES.getData() == null){
                return RES;
            }
            T convert = (T) TransSupportJavaType.tryConstructSupportTransObject(type, RES.getData());
            if(convert != null){
                RES.setData(convert);
            }else {
                if(List.class.isAssignableFrom(type)){
                    RES.setData(JSON.parseArray(RES.getData().toString()));
                }else {
                    RES.setData(JSON.parseObject(RES.getData().toString(), type));
                }
            }
            return RES;
        }catch (Throwable e){
            throw e;
        }finally {
            if(socket != null) socket.close();
        }
    }


    private enum TransSupportJavaType{
        INTEGER(Integer.class),
        STRING(String.class),
        LONG(Long.class),
        DOUBLE(Double.class),
        BIG_DECIMAL(BigDecimal.class),
        FLOAT(Float.class),
        BOOLEAN(Boolean.class),
        SHORT(Short.class),
        BYTE(Byte.class);
        public Class<?> assignClass;
        public MethodHandle instanceMethodHandle;
        TransSupportJavaType(Class<?> assignClass){
            this.assignClass = assignClass;
            try {
                if(assignClass == Boolean.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Boolean.class,"valueOf", MethodType.methodType(Boolean.class,String.class));
                }else if(assignClass == Byte.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Byte.class,"valueOf", MethodType.methodType(Byte.class,String.class));
                }else if(assignClass == Integer.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Integer.class,"valueOf", MethodType.methodType(Integer.class,String.class));
                }else if(assignClass == Float.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Float.class,"valueOf", MethodType.methodType(Float.class,String.class));
                }else if(assignClass == Double.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Double.class,"valueOf", MethodType.methodType(Double.class,String.class));
                }else if(assignClass == String.class){
                    this.instanceMethodHandle = methodLookup.findStatic(String.class,"valueOf",MethodType.methodType(String.class,Object.class));
                }else if(assignClass == BigDecimal.class){
                    this.instanceMethodHandle = methodLookup.findConstructor(BigDecimal.class, MethodType.methodType(void.class,String.class));
                }else if(assignClass == Short.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Short.class,"valueOf", MethodType.methodType(Short.class,String.class));
                }else if(assignClass == Long.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Long.class,"valueOf", MethodType.methodType(Long.class,String.class));
                }else { }
            }catch (Throwable e){
                throw new RuntimeException("Occur an error when Find string transFor MethodHandle in class : " + this.assignClass);
            }
        }

        /**
         * This method is used for Value Convert, Current support target JavaType is blow.
         * @param setterHandleParamClass Target's Fields's JavaType.
         * @param sourceObject source data.
         * @return source data convert to target data. by string construct.
         */
        public static Object tryConstructSupportTransObject(Class<?> setterHandleParamClass,Object sourceObject) {
            for (TransSupportJavaType supportJavaType : TransSupportJavaType.values()) {
                if(setterHandleParamClass.isAssignableFrom(supportJavaType.assignClass)){
                    Object transForObject = null;
                    try {
                        transForObject = supportJavaType.instanceMethodHandle.invoke(sourceObject.toString());
                    } catch (Throwable throwable) { /** Data value error,set null value. **/}
                    return transForObject;
                }
            }
            return null;
        }

        /**
         * This Method return MethodHandleEnum, used for filter MethodHandle(has filling become StringValue)
         * to SuitAble MethodHandle with Target Fields's Setter.
         * @param setterHandleParamClass Target's Fields's JavaType.
         * @return target field's string construct MethodHandle.
         */
        public static MethodHandle findStringConvertHandle(Class<?> setterHandleParamClass) {
            for (TransSupportJavaType supportJavaType : TransSupportJavaType.values()) {
                if(setterHandleParamClass.isAssignableFrom(supportJavaType.assignClass)){
                    return supportJavaType.instanceMethodHandle;
                }
            }
            return null;
        }
    }
}
